[ChatGPT] gpt-3.5-turbo でコップの音を聞き分けてみました 〜Audioデータは、128バイトの文字列に変換してプロンプトに入れてます
1 はじめに
CX 事業本部のデリバリー部の平内(SIN)です。
今回は、gpt-3.5-turbo を使用して、音を判別してみました。
最初に動作している様子です。
「私のコップ(my_cup)」をスプーンで叩いた音と、「別のコップ(unknown)」の音を聞いてChatGPTが、どちらを鳴らしたのか判別しています。
2 コップの音
2種類のコップを叩いた時の波形です。見た目ですが、結構、違いが出ていたので、「これぐらいなら判別できるかもしれない!」と思ったのが、今回のスタートです。
my_cup
unknown
3 データの正規化及び圧縮
Audioデータは、そのままでは、ちょっと無理があると思ったので、正規化で、0 から 255 の整数へ変換し、64バイトまで圧縮しています。また、先頭の無音部分をトリミングして、音の立ち上がりを揃えました。
- 正規化 (0..255 の整数で表現する)
- -1 〜 1 への正規化
- 絶対値への変換 0..1 の表現に変換する
- 256 倍して、0..255 の表現に変換する
- 整数化 float -> int
- Compress 圧縮 1/256
- Trim 先頭の無音部分の削除
以下の図は、データを変換していく過程の波形です。 詳しくは、後述のコードで、 create_text(frames):をご参照ください。
なお、圧縮は、何種類かやってみたのですが、元の波形が認識できる程度ということで、1/256 としました。
4 文字列化
圧縮したデータは、プロンプトで使用できるよう 16 進数の文字列とし、コップを叩くと128バイトの文字列が生成されます。
# 16bit の int データを 00-FF の 2 バイトで表現 def to_string(array): str = "" for n in range(array.size): str += "{:02X}".format(array[n]) return str
当初、「FF,FF,01,02,・・・」のように、カンマで区切って試していたのですが、トークンが 多くなってしまうので、データ間の区切りは、「無し」としました。「FFFF0102・・・」
5 プロンプト
ChatGPT に送るプロンプトは、以下のような形式となっています。
最初に、"system"で、データ形式と判定要領を定義し、その後は、事前にサンプリングしたデータで、"my_cup"と"unknown"を「判定例」を 3 回づつ繰り返しています。
「判定例」の繰り返しが、3回で充分というのは、ちょっと驚きでした。
[ { "role": "system", "content": "From the input that expresses the change in sound in a hexadecimal number of 2 byte, Look at similarity and reply with [my_cup] or [unknown]." }, { "role": "user", "content": "{既存のmy_cupのデータ}" }, { "role": "assistant", "content": "my_cup" }, ・・・略・・・ { "role": "user", "content": "{既存のunknownのデータ}" }, { "role": "assistant", "content": "unknown" }, ・・・略・・・ { "role": "user", "content": "{今回の録音データ}" } ]
6 実装
作成したコードです。
import os import numpy as np import pyaudio import openai openai.api_key = os.environ["OPENAI_API_KEY"] # プロンプトの生成 def create_prompt(data): order = "From the input that expresses the change in sound in a hexadecimal number of 2 byte, Look at similarity and reply with [my_cup] or [unknown]." my_cup_list = [ "FFFFDBB4B7897C685C504C3F3D312D282620201B1A1817141512121010100F0E0E0D0C0C0B0C0B0B0A0A0909080908080707070706060606060605050505050505040404040404030303030303020302000000000000", "FFFFFFC0B58B7A695C4E50403D302D2826211F1C1A1817161515121210100E0F0D0E0C0C0B0C0A0B0A0A0909090909080807070707070706060606050505050505040404040404030303030303030300000000000000", "FFFFD1BCAE86807995725F4C4D3B3629281C201C1815150E120E0E0C0B0B0A09090908080706060605050505040404050304030402030203020302020202020202020102010201010101010101010000000000000000", ] unknown_list = [ "FF722D130E070706050405030302010000000001000100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "FF651D100A060404030203010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "FF8727150C050404040203010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ] role = "role" content = "content" system = "system" user = "user" assistant = "assistant" prompt = [] prompt.append({role: system, content: order}) for line in my_cup_list: prompt.append({role: user, content: line}) prompt.append({role: assistant, content: "my_cup"}) for line in unknown_list: prompt.append({role: user, content: line}) prompt.append({role: assistant, content: "unknown"}) prompt.append({role: user, content: data}) return prompt # 音量が敷居値を超えているかどうかの判定 def is_valid(data, threshold): np_array = np.frombuffer(data, dtype=np.int16) # データは、16bit単位で判定する if np.amax(np_array) > threshold: return True return False # データの圧縮 def compress(array, split): result = np.empty(0, dtype=np.uint16) size = np.size(array) # データを分割する sub_arrays = np.array_split(array, len(array) / split) for i in range(int(size / split)): # 分割した中で、最大値を使用する max_val = np.max(sub_arrays[i]) result = np.append(result, max_val) return result # 先頭の無音部分の削除 def trim(array): max = array.size result = np.empty(0, dtype=np.uint16) trim = True for n in range(max): if trim and array[n] > 10: trim = False if trim == False: result = np.append(result, array[n]) for n in range(max - result.size): result = np.append(result, 0) return result # 16進文字列への変換 def to_string(array): str = "" for n in range(array.size): str += "{:02X}".format(array[n]) return str # 録音データからテキストデータの生成する def create_text(frames): # numpy arrayへの変換 np_array = np.frombuffer(b"".join(frames), dtype=np.int16) # -1 〜 1への正規化 np_array = (np_array) / (np.max(np_array) - np.mean(np_array)) # 絶対値への変換 0..1の表現に変換する np_array = np.abs(np_array) # 256倍して、0..256の表現に変換する np_array = np_array * 256 # 範囲制限 データを規定範囲内(0..255)に収める np_array = np.clip(np_array, 0, 255) # 整数化 float -> int np_array = np.array(np_array, dtype="int") # データの圧縮 1/256 np_array = compress(np_array, 256) # 先頭の無音部分の削除 np_array = trim(np_array) # 16進文字列に変換 return to_string(np_array) # Audio ストリームの初期化 def init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS): if stream is not None: stream.stop_stream() stream.close() return py_audio.open( format=FORMAT, channels=CHANNELS, rate=SAMPLE_RATE, input=True, frames_per_buffer=CHUNK, ) def main(): FORMAT = pyaudio.paInt16 CHANNELS = 1 SAMPLE_RATE = 44100 CHUNK = int(SAMPLE_RATE / 40) # ループ毎の時間は、 SAMPLE_RATE/40 = 25ms threshold = 2000 # MacOSのマイク音量に依存する py_audio = pyaudio.PyAudio() stream = None stream = init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS) buffers = [] print("start.") while True: data = stream.read(CHUNK) buffers.append(data) # 一定の音量を超えたら、処理を開始する if is_valid(data, threshold): frames = [] # 25ms前のデータから使用する frames.append(buffers[len(buffers) - 1]) for _ in range(14): # 25msec * 14 = 350msec 録音する frames.append(stream.read(CHUNK)) # 録音データからテキストデータの生成する str = create_text(frames) print("gpt-3.5-turbo:", end=" ") prompt = create_prompt(str) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=prompt, temperature=0 ) print(response.choices[0]["message"]["content"].strip()) except Exception as e: print("Exception:", e.args) stream = init_stream(py_audio, stream, CHUNK, SAMPLE_RATE, FORMAT, CHANNELS) buffers = [] if __name__ == "__main__": main()
7 最後に
今回は、音の聞き分けを試してみましたが、正直なところ、想像以上に正確でビックリしました。
ディープラーニングでAudioデータの判定などを行おうとすると、それなりに多くのデータを用意する必要がありますが、今回試した例では、事前に用意したサンプリングデータは、それぞれ3個でした。
このような使い方が、「ChatGPTの利用方法としてどうなのか?」 という疑問は棚上げし・・・ 色々、幅広く利用範囲を模索できればな、と思っています。